在第16天,當選擇了一個咖啡方案 (coffee plan) 時,我會在該方案周圍加上邊框。其他咖啡方案則變成非活躍狀態,並移除邊框。CoffeePlan 組件會發出目前啟用方案的名稱給 PlanPicker 組件,以便通知其他咖啡方案移除邊框。
const emit = defineEmits<{
  (e: 'selectedPlan', name: string): void
}>()
function selectPlan() {
  emit('selectedPlan', props.name)
}
CoffeePlan 組件定義了一個自訂事件,將名稱發送給 PlanPicker 組件。
selectedPlan 函數使用 selectedPlan 事件將方案名稱發送給父組件。PlanPicker 組件接收活躍的方案,並可通知其他方案它們不再是活躍狀態
<script lang="ts">
	interface Props {
        name: string;
		selectedPlan: (name: string) => void;
	}
	let { name = 'Default Plan', selectedPlan }: Props = $props();
	const handleSelectPlan = () => selectedPlan(name);
</script>
CoffeePlan 組件從 $props() 中取得 selectedPlan 函數。
PlanPicker 組件必須提供給 CoffeePlan 組件一個 prop callback,以供 handleSelectPlan 函數調用。
handleSelection 會以方案名稱呼叫 selectedPlan 函數。
@Component({
  ...
})
export class CoffeePlanComponent {
  name = input('Default Plan');
  selectedPlan = output<string>();
  selectPlan() {
    this.selectedPlan.emit(this.name());
  }
}
CoffeePlanComponent 宣告了一個 selectedPlan 輸出,用來將咖啡名稱發送給 PlanPickerComponent。name 是一個字串類型的信號輸入。
當呼叫 selectPlan 方法時,selectedPlan 會將 name getter 的結果輸出給 PlanPickerComponent。
<script setup lang="ts">
const props = defineProps({
  selected: {
    type: Boolean,
    default: false,
  },
})
</script>
將 selected 新增到 CoffeePlan 的 props 中。當 selected 為 true 時,方案為活躍狀態並帶有邊框;當 selected 為 false 時,則移除邊框。
<template>
  <div class="plan" @click="selectPlan" :class="{ 'active-plan': selected }">
    <div class="description">
      <span class="title"> {{ name }} </span>
    </div>
  </div>
</template>
div 元素動態綁定到 active-plan 類別 (class)。當 selected 為 true 時,該 CSS 類別會被啟用。否則,類別會被移除。
<script lang="ts">
	interface Props {
		name: string;
		selectedPlan: (name: string) => void;
		selected: boolean;
	}
	let { name = 'Default Plan', selectedPlan, selected }: Props = $props();
    const handleSelectPlan = () => selectedPlan(name);
</script>
同樣地,selected 標誌是從 $props 中提取的。此外,Props 介面中有一個類型為 boolean 的 selected 屬性。
<div onclick={handleSelectPlan} class={['plan', selected && 'active-plan']}>
	<div class="description">
		<span class="title"> {name} </span>
	</div>
</div>
當 selected 為 true 時,active-plan 會成為類別列表 (class list) 的一部分。否則,類別列表 (class list) 中不會包含該類別。
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
@Component({
  selector: 'app-coffee-plan',
  template: `
    <div class="plan" (click)="selectPlan()" [class]="{ 'active-plan': selected() }">
      <div class="description">
        <span class="title"> {{ name() }} </span>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CoffeePlanComponent {
  name = input('Default Plan');
  selected = input(false);
  selectedPlan = output<string>();
}
selected 是一個初始為 false 的信號輸入。
div 元素的 class 屬性綁定到一個物件。當 selected 的 getter 函式返回 true 時,active-plan 類別 (class) 會被啟用。否則,該類別 (class) 會從元素中移除,邊框也不會顯示。
// PlanPicker
<script setup lang="ts">
const selectedPlan = ref('')
function handleSelectPlan(name: string) {
  selectedPlan.value = name
}
function isSelected(plan: string) {
  return selectedPlan.value === plan
}
</script>
<template>
  <div class="plans">
    <CoffeePlan
      v-for="plan in plans"
      :key="plan"
      :name="plan"
      :selected="isSelected(plan)"
      @selectedPlan="handleSelectPlan"
    >
    </CoffeePlan>
  </div>  
</template>
當 CoffeePlan 發出 selectedPlan 事件時,handleSelectedPlan 會更新 selectedPlan ref。isSelected 函式用來判斷是否有選擇咖啡方案 (coffee plan)。接著,該函式會將結果指派給 CoffeePlan 組件的 selected 輸入。當方案被選中時,CSS 會為其添加邊框,否則咖啡方案不會顯示邊框。
<script lang="ts">
    let selectedCoffeePlan = $state('');
    const selectedPlan = (name: string) => (selectedCoffeePlan = name);
    const isSelected = (plan: string) => selectedCoffeePlan === plan;
</script>
<div class="plans">
{#each plans as plan (plan)}
    <CoffeePlan name={plan} {selectedPlan} selected={isSelected(plan)} />
{/each}
</div>
當 CoffeePlan 發出 selectedPlan 事件時,selectedPlan 會更新 selectedCoffeePlan rune。isSelected 函式用來判斷是否有選擇咖啡方案。接著,該函式會將結果指派給 CoffeePlan 組件的 selected 輸入。當方案被選中時,CSS 會為其添加邊框,否則咖啡方案不會顯示邊框。
@Component({
  selector: 'app-plan-picker',
})
export class PlanPickerComponent {
  selectedPlan = signal('');
  handleSelectPlan(name: string) {
    this.selectedPlan.set(name);
  }
  isPlanSelected(planName: string): boolean {
    return this.selectedPlan() === planName;
  }
}
<div class="plans">
    @for (plan of plans(); track plan) {
        <app-coffee-plan
            [name]="plan"                           
            (selectedPlan)="handleSelectPlan($event)"
            [selected]="isPlanSelected(plan)"
        />
      }
</div>
當 PlanPickerComponent 發出 selectedPlan 事件時,handleSelectPlan 方法會更新 selectedPlan 信號。isPlanSelected 方法用來判斷是否有選擇咖啡方案。接著,該方法會將結果指派給 CoffeePlan 組件的 selected 輸入。當方案被選中時,CSS 會為其添加邊框,否則咖啡方案不會顯示邊框。
我們成功地新增了新的 prop 和組件事件 (component event),用於在 CoffeePlan 組件和 PlanPicker 組件之間進行溝通。PlanPicker 從 selected 的值中衍生新的值,並將其傳遞給其他 CoffeePlan 組件,以動態啟用或停用 CSS 類別。